/**
 * 
 */
package logic.huffman;

import io.ImageReader;

import java.awt.image.BufferedImage;
import java.util.HashMap;

import datamodel.RGB;
import datamodel.huffman.tree.Tree;
import datamodel.huffman.tree.TreeNode;

/**
 * Creates Huffman code from a Huffman tree. Can encrypt and decrypt
 * BufferedImage's
 * 
 * @author Kevin Munk, Jakob Karolus
 * @version 1.0
 * 
 */
public class HuffmanCode {

	private HashMap<RGB, String> huffmanCode = new HashMap<RGB, String>();
	private Tree huffmanTree = null;

	/**
	 * Initiates this object.
	 * 
	 * @param tree
	 *            the huffman tree to build the code from
	 */
	public HuffmanCode(Tree tree) {
		super();

		// set internal tree
		huffmanTree = tree;

		buildHuffmanCode();
	}

	/**
	 * Retrieves the Huffman code from the internal Huffman tree. The right
	 * subtrees will be coded with ones and the left subtree will be coded with
	 * zeros.
	 */
	private void buildHuffmanCode() {
		buildHuffmanCode(huffmanTree.getRoot(), "");
	}

	/**
	 * Baut rekursiv für jede Farbe den "Code-String" aus dem Huffmantree auf.
	 * 
	 * @param currentNode aktueller Knoten (beim Aufruf Wurzel)
	 * @param BufferedString Huffman-Code-String (beim Aufruf leer)
	 */
	private void buildHuffmanCode(TreeNode currentNode, String BufferedString) {

		if (!currentNode.hasLeftNode() && !currentNode.hasRightNode()) {
			huffmanCode.put(currentNode.getRgb(), BufferedString);
		} else {
			buildHuffmanCode(currentNode.getLeftNode(), BufferedString + "0");
			buildHuffmanCode(currentNode.getRightNode(), BufferedString + "1");
		}
	}

	/**
	 * Encrypt the image data from the ImageReader with the internal Huffman
	 * code
	 * 
	 * @param imageReader the data which will be encrypted
	 * @return the image coded as an String. Does not contain other chars than
	 *         '0' and '1'.
	 */
	public String encryptImage(ImageReader imageReader) {
		String bytes = "";

		for (RGB rgb : imageReader) {
			bytes = bytes + huffmanCode.get(rgb);
		}
		return bytes;
	}

	/**
	 * Decrypt the encrypted image. If the encryptedImage does not contain
	 * enough colors, the remaining pixel won't be filled. If the encryptedImage
	 * has more colors than the height * width pixel specified, the remaining
	 * colors will be ignored.
	 * 
	 * @param encryptedImage
	 *            the encrypted image. the string must only contain '0' and '1'.
	 * @param width
	 *            the width of the returned image
	 * @param height
	 *            the height of the returned image
	 * @return the decrypted image
	 */
	public BufferedImage decryptImage(String encryptedImage, int width,
			int height) {

		int x = 0;
		int y = 0;
		String code = "";
		BufferedImage image = new BufferedImage(width, height,
				BufferedImage.TYPE_INT_ARGB);

		for (int i = 0; i < encryptedImage.length(); i++) {
			code = code + encryptedImage.charAt(i);

			if (huffmanCode.containsValue(code)) {
				RGB rgb = null;
				for (RGB el : huffmanCode.keySet()) {
					if (huffmanCode.get(el).equals(code)) {
						rgb = el;
					}
				}
				if (x < width) {
					image.setRGB(x, y, rgb.getRGBValue());
				} else if (y < height) {
					y++;
					x = 0;
					image.setRGB(x, y, rgb.getRGBValue());
				}
				x++;
				code = "";
			}
		}
		return image;
	}

	/**
	 * @return the huffmanCode a map which holds for every color the huffman
	 *         code
	 */
	public HashMap<RGB, String> getHuffmanCode() {
		return huffmanCode;
	}

	/**
	 * return true if the given color is in the huffman tree
	 * 
	 * @param color the color to search
	 * @return true if tree contains color; otherwise false
	 */
	public boolean containsColor(RGB color) {
		return huffmanCode.containsKey(color);
	}

	/**
	 * severs the given amount of levels in the stored tree to compress image
	 * information<br>
	 * if the amount is bigger than the actual number of levels nothing is done
	 * 
	 * @param count the amount of levels to sever
	 * @return true if severing was successful; otherwise false (count >= number
	 *         of levels)
	 */
	public boolean compress(int count) {
		int level = getLevel(huffmanTree.getRoot(), 0);
		level = level - count;

		if (level < 1) {
			return false;
		}

		cutting(huffmanTree.getRoot(), level, 1);
		huffmanCode.clear();
		buildHuffmanCode();
		return true;
	}

	/**
	 * Entfernt Ebenen sodass das Level des Baums mit maxLevel übereinstimmt
	 * 
	 * @param node "Wurzelknoten" von dem aus der Baum gekürzt werden soll
	 * @param maxLevel Die maximale Anzahl an Levels die der Baum haben darf
	 * @param akk Akkumulator (beim Aufruf 1)
	 */
	private void cutting(TreeNode node, int maxLevel, int akk) {
		if (akk == maxLevel && node.hasLeftNode() && node.hasRightNode()) {
			huffmanCode.remove(node.getLeftNode().getRgb());
			node.setLeftNode(null);
			huffmanCode.remove(node.getRightNode().getRgb());
			node.setRightNode(null);
			node.setRgb(new RGB(255, 255, 255, 255));
		} else {
			if (node.hasLeftNode())
				cutting(node.getLeftNode(), maxLevel, akk + 1);
			if (node.hasRightNode())
				cutting(node.getRightNode(), maxLevel, akk + 1);
		}
	}

	/**
	 * Bestimmt die Anzahl an Ebenen im Baum ab dem übergebenen Knoten als
	 * Wurzel
	 * 
	 * @param node der "Wurzelknoten" von dem aus die Ebenen bestimmt werden
	 *            sollen
	 * @param acc ein Zähler (beim Aufruf 0)
	 * @return die Anzahl an Ebenen im Baum
	 */
	private int getLevel(TreeNode node, int acc) {
		if (node == null) {
			return acc;
		}

		acc++;
		return Math.max(getLevel(node.getLeftNode(), acc),
				getLevel(node.getRightNode(), acc));
	}

	/**
	 * @return the huffmanTree
	 */
	public Tree getHuffmanTree() {
		return huffmanTree;
	}

}
